package com.hy.video.monitor.util;

import com.eatthepath.otp.TimeBasedOneTimePasswordGenerator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import lombok.val;
import org.springframework.stereotype.Component;

import javax.crypto.KeyGenerator;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.Key;
import java.security.NoSuchAlgorithmException;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.Optional;

/**
 * @program: mrcos -验证码工具类
 * @description TODO 具有时效性一次性密码多用于用户名/密码登录后，作为二次校验的“验证码”。验证码用完后即可抛弃
 *              TODO 不允许多次使用“验证码”。
 *              TODO 一次性密码是有失效期的，而且在有效期内，这个密码的生成应该是一致的。
 *              TODO 这就是基于时间的一次性密码。它是用一种当前时间作为输入的算法生成的。依赖java-otp
 * @author: 张三少
 * @create: 2022-04-25 20:37
 **/
@Slf4j
@Component
@RequiredArgsConstructor
public class TotpUtil {

    /**
     * totpUtil.getTimeStep() == TIME_STEP
     */
    private static final long TIME_STEP = 60 * 5L;// 5min
    private static final int PASSWORD_LENGTH = 6; // 密码长度
    private KeyGenerator keyGenerator;
    private TimeBasedOneTimePasswordGenerator totp;

    /*
     * 初始化代码块，Java 8 开始支持。这种初始化代码块的执行在构造函数之前
     * 准确说应该是 Java 编译器会把代码块拷贝到构造函数的最开始。
     */
    {
        try {
            totp = new TimeBasedOneTimePasswordGenerator(Duration.ofSeconds(TIME_STEP), PASSWORD_LENGTH);
            keyGenerator = KeyGenerator.getInstance(totp.getAlgorithm());
            // SHA-1 and SHA-256 需要 64 字节 (512 位) 的 key;
            // SHA512 需要 128 字节 (1024 位) 的 key
            keyGenerator.init(512);
        } catch (NoSuchAlgorithmException e) {
            log.error("没有找到算法 {}", e.getLocalizedMessage());
        }
    }

    /**
     * @param time 用于生成 TOTP 的时间
     * @return 一次性验证码
     * @throws InvalidKeyException 非法 Key 抛出异常
     *          TODO 注意这里的KEY应该是每个用户都是唯一的，因为一次性密码是有有失效期的，
     *          TODO 而且在有效期内，这个密码是根据key和时间生成的所以在这段时间内(TIME_STEP)和同一个key生成的验证码是一致的。
     *          TODO 所以需要给每个用户分配一个用户生成验证码的key。
     *          TODO 每个用户的key存储在哪里呢？
     *          TODO 可以存储在user表中，当用户登录成功后就可以再拿出该user的key生成该用户的验证码。
     */
    public String createTotp(final Key key, final Instant time) throws InvalidKeyException {
        val format = "%0" + PASSWORD_LENGTH + "d";// 配合String.format() 让验证码保证6位数字
        // 生成密码不足6位补全
        return String.format(format, totp.generateOneTimePassword(key, time));
    }

    /**
     * JDK8 新增了 Optional 类， Optional 类是一个没有子类的工具类。
     * 我们可以把 Optional 类看作是一个容器。这个容器它有两种情况：要么有值 OR 要么为null。
     * 创建一个 Optional 实例: Optional.of(T t);
     * @param strKey
     * @return
     */
    public Optional<String> createTotp(final String strKey) {
        try {
            return Optional.of(createTotp(decodeKeyFromString(strKey), Instant.now()));
        } catch (InvalidKeyException e) {
            return Optional.empty();
        }
    }

    /**
     * 验证 TOTP
     *
     * @param code 要验证的 TOTP
     * @return 输入进来的密码是否一致
     * @throws InvalidKeyException 非法 Key 抛出异常
     */
    public boolean validateTotp(final Key key, final String code) throws InvalidKeyException {
        val now = Instant.now(); // 生成验证码 key+设置的有效期
        return createTotp(key, now).equals(code);// 在设置的验证码有效期内生成的验证码一样
    }

    public Key generateKey() {
        return keyGenerator.generateKey();
    }

    /**
     * 存数据库user表的key
     * @return
     */
    public String encodeKeyToString() {
        return encodeKeyToString(generateKey());
    }

    /**
     * key编码
     * @param key
     * @return
     */
    public String encodeKeyToString(Key key) {
        return Base64.getEncoder().encodeToString(key.getEncoded());
    }

    /**
     * key解码
     * @param strKey
     * @return
     */
    public Key decodeKeyFromString(String strKey) {
        return new SecretKeySpec(Base64.getDecoder().decode(strKey), totp.getAlgorithm());
    }

    public long getTimeStepInLong() {
        return TIME_STEP;
    }

    /**
     * Duration类表示秒或纳秒时间间隔，适合处理较短的时间，
     * 需要更高的精确性。我们能使用between()方法比较两个瞬间的差：
     * Instant start = Instant.parse("2017-10-03T10:15:30.00Z");
     * Instant end = Instant.parse("2017-10-03T10:16:30.00Z");
     *
     * Duration duration = Duration.between(start, end);
     * @return
     */
    public Duration getTimeStep() {
        return totp.getTimeStep();
    }




}
